package com.bix.util.rendertests;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import com.bix.util.blizfiles.BlizzardResourceLocator;
import com.bix.util.blizfiles.blp.BLPImageLoader;
import com.bix.util.blizfiles.m2.M2AnimationSequence;
import com.bix.util.blizfiles.m2.M2Bone;
import com.bix.util.blizfiles.m2.M2File;
import com.bix.util.blizfiles.m2.M2FileException;
import com.bix.util.blizfiles.m2.M2Texture;
import com.bix.util.blizfiles.m2.M2Vertex;
import com.bix.util.blizfiles.skin.SkinFile;
import com.bix.util.blizfiles.skin.SkinFile.Submesh;
import com.bix.util.blizfiles.skin.SkinFile.TextureUnit;
import com.bix.util.blizfiles.skin.SkinFile.TriangleIndex;
import com.jme.animation.Bone;
import com.jme.animation.SkinNode;
import com.jme.app.SimpleGame;
import com.jme.image.Texture;
import com.jme.image.Texture.WrapAxis;
import com.jme.image.Texture.WrapMode;
import com.jme.input.FirstPersonHandler;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.math.Matrix4f;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.TexCoords;
import com.jme.scene.TriMesh;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.BoneDebugger;
import com.jme.util.TextureManager;
import com.jme.util.geom.BufferUtils;
import com.jme.util.resource.ResourceLocator;

/**
 * This is a class that provides the ability to load Blizzard M2 files. It is
 * the adapter between the M2 file format and the underlying jME resource
 * locator.
 * 
 * @author squid
 * 
 */
class M2FileLoader extends M2File {
	public class BonePair {
		public Bone jmeBone;
		public M2Bone m2Bone;

		public BonePair(Bone jmeBone, M2Bone m2Bone) {
			this.jmeBone = jmeBone;
			this.m2Bone = m2Bone;
		}
	}

	private static final Logger log = Logger.getLogger (M2FileLoader.class.getName ());

	private ResourceLocator locator;
	private List<Texture> textureList;
	private SkinNode theSkinNode;
	private List<BonePair> bonePairs = new ArrayList<BonePair> ();
	private FloatBuffer globalVertexBuffer;
	private FloatBuffer globalNormalBuffer;
	private FloatBuffer globalTextureCoords;

	public M2FileLoader(ResourceLocator locator) {
		this.locator = locator;
	}

	public M2FileLoader(ResourceLocator locator, String modelName, String defaultSkin) throws IOException,
			M2FileException, URISyntaxException {
		this.locator = locator;

		//
		// Load the model file.
		//
		this.load (modelName);

		//
		// Load the textures. Some textures are actually null/zero length because
		// the first texture sometimes comes from the DBC file (especially for
		// mobs/creatures). This allows the model to be used multiple times but
		// have it look slightly different based on the first texture.
		//
		textureList = new ArrayList<Texture> ();
		for (M2Texture texture : this.getTextures ()) {
			Texture t = null;
			URL blpImageUrl = null;
			if (texture.getFilename ().length () == 0) {
				blpImageUrl = this.getResource (defaultSkin);
			} else {
				blpImageUrl = this.getResource (texture.getFilename ());
			}

			if (blpImageUrl != null) {
				t = TextureManager.loadTexture (blpImageUrl, Texture.MinificationFilter.Trilinear,
						Texture.MagnificationFilter.Bilinear, 0.0f, false);
				if (t != null) {
					switch (texture.getFlags () & 3) {
					case 1:
						t.setWrap (WrapAxis.S, WrapMode.Repeat);
						break;
					case 2:
						t.setWrap (WrapAxis.T, WrapMode.Repeat);
						break;
					case 3:
						t.setWrap (WrapMode.Repeat);
						break;
					default:
						break;
					}
					textureList.add (t);
				}
			}
		}

		//
		// Setup the model's global information. This includes vertexes, normals,
		// and texture coordinates. All of the vertex index lists in the entire
		// model (i.e. skins/submeshes) will reference these lists.
		//
		// Note that the Y and Z coordinates are switched as the models in the M2
		// files are based on a Z-up coordinate system.
		//
		this.globalVertexBuffer = BufferUtils.createVector3Buffer (this.getVertices ().size ());
		this.globalNormalBuffer = BufferUtils.createVector3Buffer (this.getVertices ().size ());
		this.globalTextureCoords = BufferUtils.createVector2Buffer (this.getVertices ().size ());

		boolean zUpConvert = false;
		for (M2Vertex vertex : this.getVertices ()) {
			//
			// Add the vertex to the buffer and the normal vector to the normal
			// buffer.
			//
			globalVertexBuffer.put (vertex.getPosition ().getX ());
			globalNormalBuffer.put (vertex.getNormalVector ().getX ());
			if (zUpConvert) {
				globalVertexBuffer.put (vertex.getPosition ().getZ ());
				globalVertexBuffer.put (-vertex.getPosition ().getY ());
				globalNormalBuffer.put (vertex.getNormalVector ().getZ ());
				globalNormalBuffer.put (-vertex.getNormalVector ().getY ());
			} else {
				globalNormalBuffer.put (vertex.getNormalVector ().getY ());
				globalNormalBuffer.put (vertex.getNormalVector ().getZ ());
				globalVertexBuffer.put (vertex.getPosition ().getY ());
				globalVertexBuffer.put (vertex.getPosition ().getZ ());
			}

			//
			// Set the vertex's texture coordinate. This is the X and Y offset into
			// the texture applied to the vertex.
			//
			globalTextureCoords.put (vertex.getTextureCoordinate ().getX ());
			globalTextureCoords.put (vertex.getTextureCoordinate ().getY ());
		}
	}

	/**
	 * This method returns a fully populated and initialized jMonkeyEngine Node
	 * object that represents the model for a given skin.
	 * 
	 * @param skinIndex
	 *          The index of the skin to use for bones, etc.
	 * 
	 * @return A fully populated and initialized jMonkeyEngine Node.
	 */
	public Node getNode (int skinIndex) {
		//
		// Create a node that the model's meshes will be attached to.
		//
		Node modelNode = new Node ("M2 model");

		//
		// Now load up the faces. The face information is found in the "skins"
		// attribute of the model. There can be more than one set of faces/skins,
		// though each triangle index is an index into our GLOBAL vertex index
		// list. These lists will be used multiple times; once for each submesh
		// in the model.
		//

		//
		// Get the skin that we're rendering.
		//
		SkinFile skin = this.getSkins ().get (skinIndex);

		//
		// Get the vertex index list and triangle index lists for this skin. These
		// lists are referenced by the submeshes inside of the skins. These lists
		// contain indexes into the global vertex list.
		//
		short[] skinVertexIndexList = skin.getVertexIndexList ();
		TriangleIndex[] skinTriangleIndexes = skin.getTriangleIndexList ();

		//
		// Render the submeshes. Each submesh is a mesh that references the skin's
		// vertexes.
		//
		log.debug ("Loading the submeshes.");
		for (int submeshIndex = 0; submeshIndex < skin.getSubmeshList ().length; submeshIndex++) {
			//
			// Get the submesh to render.
			//
			Submesh submesh = skin.getSubmeshList ()[submeshIndex];
			if (submesh == null) {
				continue;
			}

			//
			// Create a trimesh that will map to this submesh, initialize it to use
			// our global lists created above, and attach it to the model node that
			// we're building.
			//
			log.debug ("Creating the trimesh for submesh #" + submeshIndex);
			TriMesh tm = new TriMesh (this.getModelName ());
			tm.setVertexBuffer (this.globalVertexBuffer);
			tm.setNormalBuffer (this.globalNormalBuffer);
			tm.setTextureCoords (new TexCoords (this.globalTextureCoords));
			modelNode.attachChild (tm);

			//
			// Create a buffer to hold enough triangle indexes to describe the mesh
			// and set it as the mesh's triangle index buffer. Then, iterate over the
			// submesh's triangles and add them to the list. The number of triangles
			// is the number of triangle vertices / 3. The submesh's triangle list
			// uses the skin's global vertexes.
			//
			// The trimesh's index buffer is a buffer that is used by the trimesh
			// to map its vertices into the global vertex list.
			//
			IntBuffer triangles = IntBuffer.allocate (submesh.getNumTriangleVertices ());
			log.debug ("  Starting triangle vertex index[" + submesh.getStartTriangle () + "]");
			short startTriangleVertex = (short) (submesh.getStartTriangle () / 3);
			for (int i = 0; i < submesh.getNumTriangleVertices () / 3; i++) {
				TriangleIndex triangle = skinTriangleIndexes[startTriangleVertex + i];
				for (short vertexIndex : triangle.getVertexIndices ()) {
					triangles.put (skinVertexIndexList[vertexIndex]);
				}
			}
			tm.setIndexBuffer (triangles);

			//
			// Build the model bone structure. Using jmeBoneList to store bones for
			// later on when we set the bone weights.
			//
			log.debug ("Building the model's bone structure.");
			for (int boneIndex = 0; boneIndex < this.getBones ().size (); boneIndex++) {
				//
				// Add the bone to our bone list. The name of the bone will be:
				// bone[index]. If the bone is the root bone, we attach it to the
				// model, otherwise, we attach it to its parent bone.
				//
				M2Bone bone = this.getBones ().get (boneIndex);
				String boneName = "bone[" + boneIndex + "]";
				log.debug ("Bone: " + boneName);
				Bone jmeBone = new Bone (boneName);

				//
				// This is a map we use to map JME bones to M2bones so we can quickly
				// calculate how a model is animated (see updateBones ()).
				//
				this.bonePairs.add (new BonePair (jmeBone, bone));

				//
				// If the bone has a parent, then attach it to the parent bone.
				//
				if (bone.getParentBoneId () != -1) {
					Bone parentBone = bonePairs.get (bone.getParentBoneId ()).jmeBone;
					log.debug ("  Attaching " + boneName + " to bone " + parentBone.getName ());
					int numChildren = parentBone.attachChild (jmeBone);
					log.debug ("    " + parentBone.getName () + " now has " + numChildren + " children.");
				}
			}

			//
			// Get the number of bones in this submesh. If there are bones, than
			// create a skin and attach it to the trimesh. Also apply the bone
			// weights for the skin's vertices.
			//
			if (submesh.getNumBones () > 1) {
				log.debug ("Setting up the skin's bone influences...");
				theSkinNode = new SkinNode ("Skin[" + submeshIndex + "]");
				modelNode.attachChild (theSkinNode);
				theSkinNode.addSkin (tm);

				//
				// Iterate over the skin's vertex index list.
				//
				for (short index : skinVertexIndexList) {
					log.debug ("skinVertexIndex = " + index);
					M2Vertex vertex = this.getVertices ().get (index);
					float totalWeight = 0;
					for (int k = 0; k < 4; k++) {
						float boneWeight = vertex.getBoneWeights ()[k];
						if (boneWeight != 0) {
							totalWeight += boneWeight;

							//
							// Get the vertex's bone index.
							//
							int boneIndex = vertex.getBoneIndices ()[k];
							Bone jmeBone = bonePairs.get (boneIndex).jmeBone;
							log.debug ("    " + jmeBone.getName () + ", boneIndex: " + boneIndex + ", weight: " + boneWeight);
							theSkinNode.addBoneInfluence (0, index, jmeBone, boneWeight);
						}
					}

					//
					// Make sure the combined total weights for the vertex was 1.
					//
					if (totalWeight != 1.0f) {
						log.warn ("Total Weight not 1.0f: " + totalWeight);
					}
				}

				//
				// Attach the root bone.
				//
				int rootBoneIndex = this.getKeyBoneLookups ()[26];
				rootBoneIndex = 0;
				log.debug ("Root Bone Index: " + rootBoneIndex);
				Bone jmeBone = bonePairs.get (rootBoneIndex).jmeBone;
				theSkinNode.attachChild (jmeBone);
				theSkinNode.setSkeleton (jmeBone);
				theSkinNode.normalizeWeights ();
				theSkinNode.regenInfluenceOffsets ();
				log.debug ("Skin Influence Count: " + theSkinNode.getInfluenceCount (0));
			}

			//
			// Locate the texture(s) that belong to this submesh.
			//
			for (TextureUnit tu : skin.getTextureUnits ()) {
				//
				// Is this the texture unit that belongs to this submesh?
				//
				if (tu.getSubmeshIndex () == submeshIndex) {
					//
					// Get the index into the global texture index list.
					//
					int textureIndex = tu.getTexture ();

					//
					// Use the index to reference the index... yah... confusing...
					//
					textureIndex = this.getTextureLookups ()[textureIndex];
					if (textureIndex < 0 || textureIndex >= this.textureList.size ()) {
						continue;
					}

					//
					// Get the texture from the list of global textures. If there wasn't
					// an entry, then use the texture at index 0.
					//
					Texture t = this.textureList.get (textureIndex);
					if (t == null) {
						t = this.textureList.get (0);
					}

					//
					// Set the texture for the submesh.
					//
					TextureState ts = DisplaySystem.getDisplaySystem ().getRenderer ().createTextureState ();
					ts.setTexture (t);
					tm.setRenderState (ts);

					//
					// Add the alpha values for the transparent node.
					//
					// TODO: Not working :(
					//
					BlendState as1 = DisplaySystem.getDisplaySystem ().getRenderer ().createBlendState ();
					as1.setBlendEnabled (true);
					as1.setSourceFunction (BlendState.SourceFunction.SourceAlpha);
					as1.setDestinationFunction (BlendState.DestinationFunction.OneMinusSourceAlpha);
					as1.setEnabled (true);
					//
					// This is f'ed up.
					//
					// tm.setRenderState(as1);
					// tm.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
				}
			}
		}

		return modelNode;
	}

	@Override
	public URL getResource (String resourceName) {
		return this.locator.locateResource (resourceName);
	}

	/**
	 * @return the locator
	 */
	public ResourceLocator getLocator () {
		return locator;
	}

	/**
	 * @return the theSkinNode
	 */
	public SkinNode getTheSkinNode () {
		return theSkinNode;
	}

	/**
	 * @return the bonePairs
	 */
	public List<BonePair> getBonePairs () {
		return bonePairs;
	}
}

/**
 * Started Date: January 12, 2009
 * 
 * This class is used to convert the Blizzard M2 file formats into jMonkeyEngine
 * compatible meshes.
 * 
 * @author Squid
 */
public class TestM2Render extends SimpleGame {
	private static final Logger log = Logger.getLogger (TestM2Render.class.getName ());

	private String modelFilename;

	private M2FileLoader theModel;
	private Node modelNode;
	private int curTime = 0;
	private int animationSequence = 0;
	private boolean singleStepAnimation = false;
	private boolean showBones = false;

	public static void main (String[] args) {
		TestM2Render app = new TestM2Render ();

		//
		// Check for command line arguments.
		//
		if (args.length > 0) {
			app.setModelFilename (args[0]);
		}

		app.setConfigShowMode (ConfigShowMode.AlwaysShow);
		app.start ();
	}

	/**
	 * This sets up the application.
	 */
	protected void simpleInitGame () {
		// KeyBindingManager.getKeyBindingManager ().add ("camera_position",
		// KeyInput.KEY_P);
		// KeyBindingManager.getKeyBindingManager ().set ("test_animate",
		// KeyInput.KEY_F6);
		KeyBindingManager.getKeyBindingManager ().set ("test_animate_forward", KeyInput.KEY_ADD);
		KeyBindingManager.getKeyBindingManager ().set ("test_animate_backward", KeyInput.KEY_SUBTRACT);
		KeyBindingManager.getKeyBindingManager ().set ("dump_bone_list", KeyInput.KEY_APOSTROPHE);
		KeyBindingManager.getKeyBindingManager ().set ("debug", KeyInput.KEY_BACKSLASH);
		KeyBindingManager.getKeyBindingManager ().set ("single_step", KeyInput.KEY_SLASH);
		KeyBindingManager.getKeyBindingManager ().set ("dump_bone_animations", KeyInput.KEY_M);
		KeyBindingManager.getKeyBindingManager ().set ("dump_cur_time", KeyInput.KEY_COMMA);
		KeyBindingManager.getKeyBindingManager ().set ("debug_bones", KeyInput.KEY_F12);

		//
		// Attempt to load the property file. If an exception occurs, just set the
		// properties to null (i.e. no property file available).
		//
		Properties props = new Properties ();
		try {
			File propertyFile = new File ("jwmv.properties");
			FileInputStream fis = new FileInputStream (propertyFile);
			props.load (fis);
		} catch (FileNotFoundException e1) {
			props = null;
		} catch (IOException e) {
			props = null;
		}

		//
		// Get the resource directories to search.
		//
		String resourceDirectories = props == null ? null : props.getProperty ("ResourceDirectories");
		if (resourceDirectories == null || resourceDirectories.length () == 0) {
			throw new RuntimeException ("Missing the ResourceDirectories property file entry.");
		}

		//
		// Set our movement speed to something a tad slower than the default.
		//
		// 
		((FirstPersonHandler) (this.input)).getKeyboardLookHandler ().setMoveSpeed (10f);

		//
		// Register the Blizzard BLP image loader.
		//
		TextureManager.registerHandler (".BLP", new BLPImageLoader ());

		//
		// Set up a resource locator that understands (or somewhat understands) the
		// Blizzard file and directory structure.
		//
		ResourceLocator locator = new BlizzardResourceLocator (resourceDirectories);

		//
		// Load up the model.
		//
		try {
			// model = new M2FileLoader(locator, "DuskwoodCoveredBridge.m2");
			String filename = this.getModelFilename ();
			if (filename == null || filename.length () == 0) {
				filename = props == null ? "deer.m2" : props.getProperty ("DefaultModel");
			}

			if (filename == null) {
				throw new RuntimeException ("Filename is null.");
			}
			
			theModel = new M2FileLoader (locator, filename, props.getProperty ("DefaultSkin", "DeerSkin.blp"));
			if (theModel == null) {
				throw new RuntimeException ("Model " + filename + " not found.");
			}

		} catch (MalformedURLException e2) {
			e2.printStackTrace ();
			return;
		} catch (URISyntaxException e2) {
			e2.printStackTrace ();
			return;
		} catch (IOException e) {
			e.printStackTrace ();
			return;
		} catch (M2FileException e) {
			e.printStackTrace ();
			return;
		}

		//
		// Get the animation sequence we're going to be rendering.
		//
		this.animationSequence = Integer.parseInt (props.getProperty ("DefaultAnimationSequence", "0"));
		if (this.animationSequence >= theModel.getAnimationSequences ().size ()) {
			log.error ("Invalid animation sequence number, reverting to 0.");
			this.animationSequence = 0;
		}

		//
		// Get the time to start the animation sequence at.
		//
		this.curTime = Integer.parseInt (props.getProperty ("DefaultAnimationStartTime", "0"));

		//
		// Get the skin that we're going to use for the model.
		//
		int skinIndex = Integer.parseInt (props.getProperty ("DefaultSkinIndex", "0"));

		//
		// Get the model's node for the skin and animation sequence.
		//
		modelNode = theModel.getNode (skinIndex);

		//
		// Update the bones starting at the given time.
		//
		updateBones (this.animationSequence, this.curTime);

		//
		// Attach the model to the root node.
		//
		rootNode.attachChild (modelNode);

		//
		// Black background.
		//
		display.getRenderer ().setBackgroundColor (ColorRGBA.cyan);

		//
		// Turn off the lights.
		//
		lightState.setEnabled (false);

		//
		// Put the camera somewhere where we can see the model.
		//
		this.cam.setLocation (new Vector3f (0.12254114f, -4.5623116f, 0.83426607f));
		this.cam.setDirection (new Vector3f (-7.112529E-5f, 0.99999976f, -7.92942E-4f));
		this.cam.setLeft (new Vector3f (-0.99595284f, 4.0745363E-10f, 0.08987802f));
		this.cam.setUp (new Vector3f (0.08987861f, 7.963432E-4f, 0.9959524f));
	}

	protected void simpleRender () {
		if (this.showBones) {
			BoneDebugger.drawBones (theModel.getTheSkinNode (), display.getRenderer ());
		}
	}

	/**
	 * @return the modelFileanme
	 */
	public String getModelFilename () {
		return this.modelFilename;
	}

	/**
	 * @param modelFileanme
	 *          the modelFileanme to set
	 */
	public void setModelFilename (String modelFilename) {
		this.modelFilename = modelFilename;
	}

	/**
	 * This method is called to update the model's bones for a particular
	 * animation sequence and time.
	 * 
	 * @param animationSequence
	 *          The animation sequence to use.
	 * @param animationTime
	 *          The time into the animation sequence.
	 */
	protected void updateBones (int animationSequence, int animationTime) {
		//
		// Recalculate the bone matrices.
		//
		for (M2FileLoader.BonePair bonePair : this.theModel.getBonePairs ()) {
			bonePair.m2Bone.calculateMatrixJme (animationSequence, animationTime);
		}

		//
		// Iterate over the bone map transforming each bone according to which of
		// the model's bones it is attached to.
		//
		for (M2FileLoader.BonePair bonePair : this.theModel.getBonePairs ()) {
			//
			// Get the model's bone that this bone references and calculate its
			// transformation matrix.
			//
			com.jme.math.Matrix4f mat = bonePair.m2Bone.lastCalcMatrix;
			if (mat != null) {
				//
				// Set the transformation for the given bone.
				//
				bonePair.jmeBone.setLocalTranslation (mat.toTranslationVector ());
				bonePair.jmeBone.setLocalRotation (mat.toRotationQuat ());
				bonePair.jmeBone.setLocalScale (bonePair.m2Bone.lastScaling);
			}
		}
		if (theModel != null && theModel.getTheSkinNode () != null) {
			theModel.getTheSkinNode ().updateSkin ();
		}
	}

	protected void dumpBones () {
	}

	protected void simpleUpdate () {
		M2AnimationSequence anim = theModel.getAnimationSequences ().get (this.animationSequence);

		//
		// Forward through the animation.
		//
		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("test_animate_forward", !singleStepAnimation)) {
			this.curTime = (this.curTime + 1) % anim.getLength ();
			updateBones (this.animationSequence, this.curTime % anim.getLength ());
		}

		//
		// Backward through the animation.
		//
		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("test_animate_backward", !singleStepAnimation)) {
			if (this.curTime > 0) {
				this.curTime = (this.curTime - 1) % anim.getLength ();
				updateBones (this.animationSequence, this.curTime % anim.getLength ());
			}
		}

		//
		// Write out the camera's position.
		//
		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("camera_position", false)) {
			log.info ("direction: " + this.cam.getDirection ());
			log.info ("location: " + this.cam.getLocation ());
			log.info ("left: " + this.cam.getLeft ());
			log.info ("up: " + this.cam.getUp ());
		}

		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("debug", false)) {
			Logger.getRootLogger ().setLevel (Level.DEBUG);
			log.debug ("Logging switched to debug.");
		}

		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("single_step", false)) {
			singleStepAnimation = !singleStepAnimation;
		}

		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("debug_bones", false)) {
			showBones = !showBones;
		}

		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("dump_cur_time", false)) {
			log.info ("curTime: " + this.curTime);
		}

		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("dump_bone_animations", false)) {
			log.info ("Bone Animation Data for Animation Sequence #" + this.animationSequence + ", Time: " + this.curTime);
			//
			// Iterate over the bone map transforming each bone according to which of
			// the model's bones it is attached to.
			//
			int boneIndex = 0;
			for (M2FileLoader.BonePair bonePair : this.theModel.getBonePairs ()) {
				//
				// Set the transformation for the given bone.
				//
				M2Bone m2Bone = bonePair.m2Bone;

				//
				// Get the model's bone that this bone references and calculate its
				// transformation matrix.
				//
				Matrix4f mat = bonePair.m2Bone.calculateMatrixJme (animationSequence, this.curTime);
				if (mat != null) {
					log.info ("Bone Index #" + boneIndex++ + " (Name: " + m2Bone.getName () + ") transformation values:");
					if (m2Bone.getParentBone () != null) {
						log.info ("  Parent Bone Name: " + m2Bone.getParentBone ().getName ());
					} else {
						log.info ("  No Parent Bone");
					}

					int frameNum = m2Bone.getTransformationAnimation ().getFrameNumber (this.animationSequence, this.curTime);
					if (frameNum != -1) {
						log.info ("  Translation for time " + this.curTime + ", frame #" + frameNum + ": "
								+ mat.toTranslationVector ());
					} else {
						log.info ("  No translation for time " + this.curTime);
					}

					frameNum = m2Bone.getRotationAnimation ().getFrameNumber (this.animationSequence, this.curTime);
					if (frameNum != -1) {
						log.info ("  Rotation for time " + this.curTime + ", frame #" + frameNum + ": " + mat.toRotationMatrix ());
					} else {
						log.info ("  No rotation for time " + this.curTime);
					}

					frameNum = m2Bone.getScalingAnimation ().getFrameNumber (this.animationSequence, this.curTime);
					if (frameNum != -1) {
						Vector3f v = m2Bone.getScalingAnimation ().getKeyFrameDataValues (this.animationSequence)[frameNum];

						log.info ("  Scale for time " + this.curTime + ", frame #" + frameNum + ": (" + v.x + "," + v.y + "," + v.z
								+ ")");
					} else {
						log.info ("  No scaling for time " + this.curTime);
					}
				} else {
					log.info ("Bone Index #" + boneIndex++ + " (" + m2Bone.getName () + ") has no animation matrix.");
				}
			}
		}

		//
		// Dump the model's bone list.
		//
		if (KeyBindingManager.getKeyBindingManager ().isValidCommand ("dump_bone_list", false)) {
			log.info ("There are " + this.theModel.getBonePairs ().size () + " bones in this skin.");
			int boneIndex = 0;
			for (M2FileLoader.BonePair pair : this.theModel.getBonePairs ()) {
				log.info ("Bone #" + boneIndex++);
				log.info (pair.jmeBone.toString ());
			}
		}
	}
}
